package com.avengers.common.xml;

import com.avengers.common.xml.annotation.XmlElementName;
import com.avengers.common.xml.annotation.XmlIndex;
import com.avengers.common.xml.annotation.XmlProperty;
import com.avengers.common.xml.annotation.XmlValue;
import com.avengers.core.bean.enums.SymbolEnum;
import com.avengers.core.kit.StringKit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author yu.wang
 * @since 2020-01-16 17:48
 **/
public class XmlTransfer {
    private static final Logger logger = LoggerFactory.getLogger(XmlTransfer.class);
    private static final String METHOD_GET = "get";
    private final Object root;

    public XmlTransfer(Object serviceRequest) {
        this.root = serviceRequest;
    }

    @Override
    public String toString() {
        return toString(Boolean.FALSE);
    }

    public String toString(boolean formatXml) {
        Class<?> tClass = this.root.getClass();
        StringBuilder xmlSB = new StringBuilder();
        xmlSB.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        if (formatXml) {
            xmlSB.append(SymbolEnum.reLine);
        }
        XmlElementName elementName = (XmlElementName) tClass.getAnnotation(XmlElementName.class);
        String rootEleName = null != elementName ? elementName.value() : getFieldNameFromMethodName(tClass.getSimpleName());
        xmlSB.append(SymbolEnum.angleBracketsLeft)
                .append(rootEleName);
        appendXmlProperty(xmlSB, tClass.getMethods(), this.root, this.root.getClass());
        xmlSB.append(SymbolEnum.angleBracketsRight);
        handlerGetMethod(xmlSB, tClass.getMethods(), this.root, formatXml);
        if (formatXml) {
            xmlSB.append(SymbolEnum.reLine);
        }
        xmlSB.append(SymbolEnum.angleBracketsLeft)
                .append(SymbolEnum.sprit)
                .append(rootEleName)
                .append(SymbolEnum.angleBracketsRight);
        return xmlSB.toString();
    }

    private void appendXmlProperty(StringBuilder xmlSB, Method[] methods, Object classObject, Class<?> clazz) {
        if (null == methods || methods.length < 1) {
            return;
        }
        List<Method> propertyMethods = Stream.of(methods).filter(method -> {
            if (!method.getName().startsWith("get") || 0 != method.getParameterCount() || method.getName().equals("getClass")) {
                return false;
            }
            String fieldName = getFieldNameFromMethodName(method.getName());
            Field field = getField(clazz, fieldName);
            if (null == field) {
                return false;
            }
            XmlProperty property = field.getAnnotation(XmlProperty.class);
            return null != property;
        }).sorted(Comparator.comparingInt(method -> {
            String fieldName = getFieldNameFromMethodName(method.getName());
            Field field = getField(clazz, fieldName);
            if (null == field) {
                return 0;
            }
            XmlProperty xmlIndex = field.getAnnotation(XmlProperty.class);
            return xmlIndex.value();
        })).collect(Collectors.toList());
        for (Method propertyMethod : propertyMethods) {
            appendXmlProperty(xmlSB, propertyMethod, classObject, clazz);
        }
    }

    private void appendXmlProperty(StringBuilder xmlSB, Method method, Object classObject, Class<?> clazz) {
        if (!method.getName().startsWith("get") || 0 != method.getParameterCount() || method.getName().equals("getClass")) {
            return;
        }
        String fieldName = getFieldNameFromMethodName(method.getName());
        Field field = getField(clazz, fieldName);
        if (null == field) {
            return;
        }
        XmlElementName elementName = field.getAnnotation(XmlElementName.class);
        try {
            Object subObject = method.invoke(classObject);
            if (null == subObject) {
                return;
            }
            Class<?> subClass = subObject.getClass();
            if (!(subObject instanceof CharSequence)
                    && !(subObject instanceof Number)
                    && !(subObject instanceof Boolean)
                    && !subClass.isPrimitive()
                    && !subClass.isEnum()) {
                return;
            }
            xmlSB.append(SymbolEnum.space)
                    .append(null != elementName ? elementName.value() : fieldName)
                    .append(SymbolEnum.equalSign)
                    .append(SymbolEnum.quotesDouble)
                    .append(subObject)
                    .append(SymbolEnum.quotesDouble);
        } catch (Exception e) {
            logger.error("transfer request_{} to xml error: {}", root.getClass().getSimpleName(), e.getMessage(), e);
        }
    }

    private Field getField(String fieldName, Class<?> clazz) {
        try {
            return clazz.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            return null;
        }
    }

    private Field getField(Class<?> clazz, String fieldName) {
        Field field = getField(fieldName, clazz);
        if (null == field) {
            Class<?> parent = clazz.getSuperclass();
            while (null != parent && null == field) {
                field = getField(fieldName, parent);
                if (null == field) {
                    parent = parent.getSuperclass();
                }
            }
        }
        return field;
    }

    private void handlerGetMethod(StringBuilder xmlSB, Method[] methods, Object classObject, boolean formatXml) {
        handlerGetMethod(xmlSB, methods, classObject, 1, formatXml, false);
    }

    private boolean handlerGetMethod(StringBuilder xmlSB, Method[] methods, Object classObject, int level, boolean formatXml, boolean isList) {
        final Class<?> clazz = classObject.getClass();
        Arrays.sort(methods, Comparator.comparingInt((Method sortMethod) -> {
            String fieldName = getFieldNameFromMethodName(sortMethod.getName());
            Field field = getField(clazz, fieldName);
            if (null == field) {
                return 0;
            }
            XmlIndex xmlIndex = field.getAnnotation(XmlIndex.class);
            if (null != xmlIndex) {
                return xmlIndex.value();
            }
            return 0;
        }));
        StringBuilder tabSb = new StringBuilder();
        for (int i = 0; i < level; i++) {
            for (int j = 0; j < 4; j++) {
                tabSb.append(SymbolEnum.space);
            }
        }
        String eleName = null;
        if (isList) {
            XmlElementName elementName = clazz.getAnnotation(XmlElementName.class);
            eleName = null != elementName ? elementName.value() : getFieldNameFromMethodName(clazz.getSimpleName());
        }

        boolean needRight = false;
        if (StringKit.notBlank(eleName)) {
            level++;
            if (formatXml) {
                xmlSB.append(SymbolEnum.reLine).append(tabSb);
            }
            needRight = true;
            xmlSB.append(SymbolEnum.angleBracketsLeft)
                    .append(eleName);
            appendXmlProperty(xmlSB, clazz.getMethods(), classObject, clazz);
        }
        if (needRight) {
            xmlSB.append(SymbolEnum.angleBracketsRight);
        }
        boolean needFormart = formatXml;
        for (Method method : methods) {
            needFormart = appendXmlSubElement(xmlSB, method, classObject, level, formatXml, clazz) && needFormart;
        }
        if (StringKit.notBlank(eleName)) {
            if (needFormart) {
                xmlSB.append(SymbolEnum.reLine).append(tabSb);
            }
            xmlSB.append(SymbolEnum.angleBracketsLeft)
                    .append(SymbolEnum.sprit)
                    .append(eleName)
                    .append(SymbolEnum.angleBracketsRight);
        }
        return needFormart;
    }

    private boolean appendXmlSubElement(StringBuilder xmlSB, Method method, Object classObject, int level, boolean formatXml, Class<?> clazz) {
        StringBuilder tabSb = new StringBuilder();
        for (int i = 0; i < level; i++) {
            for (int j = 0; j < 4; j++) {
                tabSb.append(SymbolEnum.space);
            }
        }
        if (!method.getName().startsWith("get") || 0 != method.getParameterCount() || method.getName().equals("getClass")) {
            return formatXml;
        }
        String fieldName = getFieldNameFromMethodName(method.getName());
        Field field = getField(clazz, fieldName);
        if (null == field) {
            return formatXml;
        }
        XmlProperty property = field.getAnnotation(XmlProperty.class);
        if (null != property) {
            return formatXml;
        }
        XmlElementName xmlElementName = field.getAnnotation(XmlElementName.class);
        XmlValue xmlValue = field.getAnnotation(XmlValue.class);
        boolean needFormatXmlForSubElement = formatXml;
        try {
            Object subObject = method.invoke(classObject);
            if (null == subObject) {
                return formatXml;
            }
            Class<?> subClass = subObject.getClass();
            String elementName = null != xmlElementName ? xmlElementName.value() : fieldName;
            if (null == xmlValue) {
                if (formatXml) {
                    xmlSB.append(SymbolEnum.reLine).append(tabSb);
                }
                xmlSB.append(SymbolEnum.angleBracketsLeft)
                        .append(elementName);

                appendXmlProperty(xmlSB, subClass.getMethods(), subObject, subClass);
                xmlSB.append(SymbolEnum.angleBracketsRight);
            }
            int nextLevel = level + 1;
            boolean reline = true;
            if (subObject instanceof CharSequence
                    || subObject instanceof Number
                    || subObject instanceof Boolean
                    || subClass.isPrimitive()
                    || subClass.isEnum()) {
                xmlSB.append(subObject);
                reline = false;
            } else if (subClass.isArray()) {
                int valueLength = Array.getLength(subObject);
                for (int i = 0; i < valueLength; i++) {
                    Object arraySubObject = Array.get(subObject, i);
                    Class<?> arraySubClass = arraySubObject.getClass();
                    needFormatXmlForSubElement = handlerGetMethod(xmlSB, arraySubClass.getMethods(), arraySubObject, nextLevel, formatXml, true) && needFormatXmlForSubElement;
                }
            } else if (subObject instanceof Iterable) {
                for (Object iteratorSubObject : ((Iterable<?>) subObject)) {
                    Class<?> iteratorSubClass = iteratorSubObject.getClass();
                    needFormatXmlForSubElement = handlerGetMethod(xmlSB, iteratorSubClass.getMethods(), iteratorSubObject, nextLevel, formatXml, true) && needFormatXmlForSubElement;
                }
            } else {
                needFormatXmlForSubElement = handlerGetMethod(xmlSB, subClass.getMethods(), subObject, nextLevel, formatXml, false) && needFormatXmlForSubElement;
            }
            if (null == xmlValue) {
                if (needFormatXmlForSubElement && reline) {
                    xmlSB.append(SymbolEnum.reLine).append(tabSb);
                }
                xmlSB.append(SymbolEnum.angleBracketsLeft)
                        .append(SymbolEnum.sprit)
                        .append(elementName)
                        .append(SymbolEnum.angleBracketsRight);
            }
        } catch (Exception e) {
            logger.error("transfer request_{} to xml error: {}", root.getClass().getSimpleName(), e.getMessage(), e);
        }
        return null == xmlValue && needFormatXmlForSubElement;
    }

    private String getFieldNameFromMethodName(String methodName) {
        String name = methodName.replace(METHOD_GET, SymbolEnum.empty.string);
        return String.valueOf(name.charAt(0)).toLowerCase() + name.substring(1);
    }
}
